//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkScripts
#include <NilkinsScripts/Log/LogManager.h>

#include <NilkinsScripts/Environments/Functions/Function.h>

#include <NilkinsScripts/Environments/UserTypes/ArrayAccessorDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserTypeFieldDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserType.h>

#include <NilkinsScripts/Environments/Environment.h>
#include <NilkinsScripts/Environments/EnvironmentManager.h>

#include <NilkinsScripts/Interpreters/Interpreter.h>

#include <NilkinsScripts/Scripts/Script.h>
#include <NilkinsScripts/Scripts/ScriptManager.h>

// Standards
#include <memory>

/// Structures -------------------------------

// Class used as a reference for our user type
class Data
{
	public :

		std::string _label = "I" ;
		int _i = 0 ;

		int _array [3] = {5, 4, 3} ;

		// An example of reference to a script object
		Data* _friend = nullptr ;
		nkScripts::ScriptObjectReference _friendRef ;
} ;

/// Functions --------------------------------

nkScripts::UserType* prepareDataUserType (nkScripts::Environment* env)
{
	// Create the user type
	nkScripts::UserType* type = env->setUserType("nkTutorial::Data") ;

	// Make its constructor and destructor
	type->setConstructor
	(
		[] (const nkScripts::DataStack& stack) -> void*
		{
			return new Data () ;
		}
	) ;

	type->setDestructor
	(
		[] (void* data) -> void
		{
			delete (Data*)data ;
		}
	) ;

	// Type is ready for usage
	return type ;
}

void demonstrateLossOfDataThroughGC (nkScripts::Environment* env, nkScripts::Script* script)
{
	// First, create an object through the script
	script->unload() ;
	script->setSources
	(
		R"eos(
			-- Creating a data variable in the script, meaning the environment is the owner
			local data = nkTutorial.Data.new() ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Then, retrieve the data pointer
	Data* dataPtr = (Data*)env->getObject("data", "nkTutorial::Data") ;
	// At this point, we can interact with dataPtr directly in C++

	// New script freeing the data variable
	script->unload() ;
	script->setSources
	(
		R"eos(
			data = nil ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// At that point, dataPtr is dangerous
	// It will cause a segfault whenever the garbage collection sees that the only reference to the Data object has been freed
}

void demonstrateReferenceSafe (nkScripts::Environment* env, nkScripts::Script* script)
{
	// First, create an object through the script
	script->unload() ;
	script->setSources
	(
		R"eos(
			-- Creating a data variable in the script, meaning the environment is the owner
			local data = nkTutorial.Data.new() ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Then, retrieve the data pointer, but with a reference this time
	Data* dataPtr = (Data*)env->getObject("data", "nkTutorial::Data") ;
	nkScripts::ScriptObjectReference dataRef = env->getVar("Data") ;
	// At this point, we can interact with dataPtr directly in C++

	// New script freeing the data variable
	script->unload() ;
	script->setSources
	(
		R"eos(
			data = nil ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// This time, using dataPtr is safe
	// The dataRef object is a reference over the object itself, the one that was pointed by variable data
	// This ScriptObjectReference participates in the reference counting and as long as it's alive, will prevent the object deletion
}

void demonstrateFunctionReference (nkScripts::Environment* env, nkScripts::Script* script)
{
	// Declare a function in our script
	script->unload() ;
	script->setSources
	(
		R"eos(
			function printCall ()
				print("Lua called !") ;
			end
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Retrieve a reference over it
	nkScripts::ScriptObjectReference functionRef = env->getScriptFunction("printCall") ;
	// At this point, we can interact with dataPtr directly in C++

	// And now use it to call the object
	nkScripts::DataStack output ;
	env->callScriptFunction(functionRef, nkScripts::DataStack(), output) ;
}

void demonstrateUnsafeSetterLogic (nkScripts::Environment* env, nkScripts::Script* script)
{
	// Append a setter function to our environment
	nkScripts::UserType* type = env->getUserType("nkTutorial::Data") ;

	nkScripts::Function* setter = type->addMethod("setFriend") ;

	// We need a user data as the parameter to keep it as a friend
	setter->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::USER_DATA_PTR, "nkTutorial::Data") ;

	setter->setFunction
	(
		[] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue
		{
			// Retrieve both pointers
			Data* data0 = (Data*)stack[0]._valUser._userData ;
			Data* data1 = (Data*)stack[1]._valUser._userData ;

			// Set the friend pointer alone
			data0->_friend = data1 ;

			return nkScripts::OutputValue::VOID ;
		}
	) ;

	script->unload() ;
	script->setSources
	(
		R"eos(
			local data0 = nkTutorial.Data.new() ;
			local data1 = nkTutorial.Data.new() ;

			data0:setFriend(data1) ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Now data0 keeps the pointer over data1
	// But this is unsafe : what would happend if data1 was set to nil ?
	// Then data1 would be garbage collected, leaving a dangling pointer within data0
}

void demonstrateSafeSetterLogic (nkScripts::Environment* env, nkScripts::Script* script)
{
	// Append a setter function to our environment
	nkScripts::UserType* type = env->getUserType("nkTutorial::Data") ;

	nkScripts::Function* setter = type->addMethod("setFriend") ;

	// We need a user data to keep as a friend, but this time with a reference
	setter->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::USER_DATA, "nkTutorial::Data") ;

	setter->setFunction
	(
		[] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue
		{
			// Retrieve both pointers
			Data* data0 = (Data*)stack[0]._valUser._userData ;
			Data* data1 = (Data*)stack[1]._valUser._userData ;

			// This time also retrieve the reference created by the USER_DATA type
			nkScripts::ScriptObjectReference data1Ref = stack[1]._valUser._objectRef ;

			// Set the friend pointer alone
			data0->_friend = data1 ;
			data0->_friendRef = data1Ref ;

			return nkScripts::OutputValue::VOID ;
		}
	) ;

	script->unload() ;
	script->setSources
	(
		R"eos(
			local data0 = nkTutorial.Data.new() ;
			local data1 = nkTutorial.Data.new() ;

			data0:setFriend(data1) ;
		)eos"
	) ;
	script->load() ;
	env->execute(*script) ;

	// Now that we correctly set the reference, if data1 is set to nil, it won't be garbage collected
	// Furthermore, if something sets the pointer again, the reference will be reset, letting the garbage collection do its work
}

/// Main -------------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkScripts::LogManager::getInstance()->setReceiver(logger.get()) ;

	// Create our environment
	nkScripts::Environment* env = nkScripts::EnvironmentManager::getInstance()->createOrRetrieve("firstEnv") ;
	env->setEnvironmentFor(nkScripts::INTERPRETER::LUA) ;

	// Basis for the user type
	nkScripts::UserType* type = prepareDataUserType(env) ;

	// Create a new data object
	nkScripts::Script* script = nkScripts::ScriptManager::getInstance()->createOrRetrieve("firstScript") ;
	script->setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;
	
	demonstrateLossOfDataThroughGC(env, script) ;
	demonstrateReferenceSafe(env, script) ;
	demonstrateFunctionReference(env, script) ;
	demonstrateUnsafeSetterLogic(env, script) ;
	demonstrateSafeSetterLogic(env, script) ;

	// Small pause to be able to witness the console
	system("pause") ;

	return 0 ;
}